package com.googlecode.restitory.gae.hook;

import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.PublicKey;
import java.security.Security;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.EncodedKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.googlecode.restitory.gae.BinaryContent;
import com.googlecode.restitory.gae.Request;
import com.googlecode.restitory.gae.RequestJsoner;
import com.googlecode.restitory.gae.Response;
import com.googlecode.restitory.gae.Type;

public class CryptoHook extends AbstractHook {

	public static String[] getServiceTypes() {
		Set<String> result = new HashSet<String>();
		Provider[] providers = Security.getProviders();
		for (int i = 0; i < providers.length; i++) {
			Set<Object> keys = providers[i].keySet();
			for (Iterator<Object> it = keys.iterator(); it.hasNext();) {
				String key = (String) it.next();
				key = key.split(" ")[0];
				if (key.startsWith("Alg.Alias.")) {
					key = key.substring(10);
				}
				int ix = key.indexOf('.');
				result.add(key.substring(0, ix));
			}
		}
		return result.toArray(new String[result.size()]);
	}

	public static String[] getCryptoImpls(String serviceType) {
		Set<String> result = new HashSet<String>();
		Provider[] providers = Security.getProviders();
		for (int i = 0; i < providers.length; i++) {
			Set<Object> keys = providers[i].keySet();
			for (Iterator<Object> it = keys.iterator(); it.hasNext();) {
				String key = (String) it.next();
				key = key.split(" ")[0];
				if (key.startsWith(serviceType + ".")) {
					result.add(key.substring(serviceType.length() + 1));
				} else if (key.startsWith("Alg.Alias." + serviceType + ".")) {
					result.add(key.substring(serviceType.length() + 11));
				}
			}
		}
		return result.toArray(new String[result.size()]);
	}

	public Response prepare(Request request) {
		if (!getAdapter().exists("/_/crypto/key/master/private")) {
			handle(new Request(Type.PUT, "/_/crypto/key/master"));
		}
		return getChain().execute(request);
	}

	private Response checkSign(Request request, Chain chain) {
		try {
			String pathKey = request.getParams().getFirst("k");
			String codedSign = request.getParams().getFirst("s");
			byte[] sign = new BinaryContent().parse(codedSign).getValue();
			byte[] keyBytes = chain.copy().execute(new Request(Type.GET, pathKey)).getContent().toByteArray();
			KeyFactory keyFactory = KeyFactory.getInstance("RSA");
			EncodedKeySpec pks = new X509EncodedKeySpec(keyBytes);
			PublicKey pk = keyFactory.generatePublic(pks);
			Signature signature = Signature.getInstance("SHA1withRSA");
			signature.initVerify(pk);
			signature.update(request.getContent().toByteArray());
			if (signature.verify(sign)) {
				return new Response(request).setCode(200);
			}
			return new Response(request).setCode(520).setMessage("bad sign");
		} catch (NoSuchAlgorithmException e) {
			throw new RuntimeException(e);
		} catch (InvalidKeySpecException e) {
			throw new RuntimeException(e);
		} catch (InvalidKeyException e) {
			throw new RuntimeException(e);
		} catch (SignatureException e) {
			throw new RuntimeException(e);
		}
	}

	private Response sign(Request request, Chain chain) {
		try {
			String pathKey = request.getParams().getFirst("k");
			Response respKey = chain.copy().execute(new Request(Type.GET, pathKey));
			if (respKey.getCode() != 200) {
				throw new RuntimeException("key load error: " + respKey);
			}
			byte[] keyBytes = respKey.getContent().toByteArray();
			KeyFactory keyFactory = KeyFactory.getInstance("RSA");
			EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(keyBytes);
			PrivateKey pk = keyFactory.generatePrivate(privateKeySpec);
			Signature signature = Signature.getInstance("SHA1withRSA");
			signature.initSign(pk);
			signature.update(request.getContent().toByteArray());
			byte[] raw = signature.sign();
			String coded = new BinaryContent(raw).toString();
			return new Response(request).setCode(200).setContentType("text/plain").setContent(coded);
		} catch (NoSuchAlgorithmException e) {
			throw new RuntimeException(e);
		} catch (InvalidKeySpecException e) {
			throw new RuntimeException(e);
		} catch (InvalidKeyException e) {
			throw new RuntimeException(e);
		} catch (SignatureException e) {
			throw new RuntimeException(e);
		}
	}

	private Response key(Request request, Chain chain) {
		Type type = request.getType();
		switch (type) {
		case GET:
			return getKey(request, chain);
		case DELETE:
			return deleteKey(request, chain);
		case PUT:
			return putKey(request, chain);
		}
		throw new RuntimeException("unknown type: " + type);
	}

	private Response getKey(Request request, Chain chain) {
		String[] split = request.getPath().split("/");
		if (split.length == 5) {
			throw new RuntimeException("not implemented yet");
		} else {
			return chain.execute(request);
		}
	}

	private Response deleteKey(Request request, Chain chain) {
		chain.copy().execute(request.copy().appendPath("public"));
		chain.copy().execute(request.copy().appendPath("private"));
		return chain.execute(request);
	}

	private Response putKey(Request request, Chain chain) {
		String[] split = request.getPath().split("/");
		if (split.length == 5) {
			return genKey(request, chain);
		} else {
			return chain.execute(request);
		}
	}

	private Response genKey(Request request, Chain chain) {
		try {
			KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
			keyGen.initialize(1024);
			KeyPair keypair = keyGen.genKeyPair();
			PrivateKey privateKey = keypair.getPrivate();
			PublicKey publicKey = keypair.getPublic();

			put(chain, request, "private", "application/octet-stream", privateKey.getEncoded());
			put(chain, request, "public", "application/octet-stream", publicKey.getEncoded());

			return new Response(request).setCode(200).setContentType("text/plain").setContent(request.getPath());
		} catch (NoSuchAlgorithmException e) {
			throw new RuntimeException(e);
		}
	}

	private void put(Chain chain, Request request, String append, String contentType, byte[] encoded) {
		Response resp = chain.copy().execute(
				request.copy().appendPath(append).setContentType(contentType).setContent(encoded));
		if (!resp.getCode().equals(200)) {
			throw new RuntimeException("error: " + resp);
		}
	}

	private Response getServices(Request request) {
		JsonObject obj = new JsonObject();
		String[] services = getServiceTypes();
		for (String service : services) {
			JsonArray array = new JsonArray();
			obj.add(service, array);
			String[] types = getCryptoImpls(service);
			for (String type : types) {
				array.add(new JsonPrimitive(type));
			}
		}
		String format = RequestJsoner.format(obj);
		return new Response(request).setCode(200).setContentType("application/json").setContent(format);
	}

	@Override
	public Response handle(Request request) {
		String path = request.getPath();
		if (path.equals("/_/crypto/list")) {
			return getServices(request);
		}
		if (path.startsWith("/_/crypto/key")) {
			return key(request, getChain());
		}
		if (path.equals("/_/crypto/sign")) {
			return sign(request, getChain());
		}
		if (path.equals("/_/crypto/checkSign")) {
			return checkSign(request, getChain());
		}
		return getChain().execute(request);
	}

}
